package com.bradrydzewski.gwtgantt.table;

import com.bradrydzewski.gwtgantt.model.Task;
import com.google.gwt.cell.client.AbstractEditableCell;
import com.google.gwt.cell.client.ValueUpdater;
import com.google.gwt.core.client.GWT;
import com.google.gwt.dom.client.Element;
import com.google.gwt.dom.client.EventTarget;
import com.google.gwt.dom.client.InputElement;
import com.google.gwt.dom.client.NativeEvent;
import com.google.gwt.event.dom.client.KeyCodes;
import com.google.gwt.safehtml.client.SafeHtmlTemplates;
import com.google.gwt.safehtml.client.SafeHtmlTemplates.Template;
import com.google.gwt.safehtml.shared.SafeHtml;
import com.google.gwt.safehtml.shared.SafeHtmlBuilder;
import com.google.gwt.text.shared.SafeHtmlRenderer;
import com.google.gwt.text.shared.SimpleSafeHtmlRenderer;

/**
 * An editable text cell. Click to edit, escape to cancel, return to commit.
 * 
 * Important TODO: This cell still treats its value as HTML for rendering
 * purposes, which is entirely wrong. It should be able to treat it as a proper
 * string (especially since that's all the user can enter).
 */
public abstract class TaskGridNameCell extends AbstractEditableCell<Task, TaskGridNameCell.ViewData> {

	interface Template extends SafeHtmlTemplates {
		@Template("<input type=\"text\" value=\"{0}\" tabindex=\"-1\"></input>")
		SafeHtml input(String value);

		@Template("<div><button class=\"{0}\" style=\"margin-left:{1}px\"></button>{2}</div>")
		SafeHtml label(String style, int marginLeft, String value);
	}

	/**
	 * The view data object used by this cell. We need to store both the text
	 * and the state because this cell is rendered differently in edit mode. If
	 * we did not store the edit state, refreshing the cell with view data would
	 * always put us in to edit state, rendering a text box instead of the new
	 * text string.
	 */
	static class ViewData {

		private boolean isEditing;

		/**
		 * If true, this is not the first edit.
		 */
		private boolean isEditingAgain;

		/**
		 * Keep track of the original value at the start of the edit, which
		 * might be the edited value from the previous edit and NOT the actual
		 * value.
		 */
		private String original;

		private String text;
		
		private Task task;

		/**
		 * Construct a new ViewData in editing mode.
		 * 
		 * @param text
		 *            the text to edit
		 */
		public ViewData(Task task) {
			this.task = task;
			this.original = task.getName();
			this.text = task.getName();
			this.isEditing = true;
			this.isEditingAgain = false;
		}

		@Override
		public boolean equals(Object o) {
			if (o == null) {
				return false;
			}
			ViewData vd = (ViewData) o;
			return equalsOrBothNull(original, vd.original)
					&& equalsOrBothNull(text, vd.text)
					&& isEditing == vd.isEditing
					&& isEditingAgain == vd.isEditingAgain;
		}

		public String getOriginal() {
			return original;
		}

		public String getText() {
			return text;
		}
		
		public Task getTask() {
			return task;
		}

		@Override
		public int hashCode() {
			return original.hashCode() + text.hashCode()
					+ Boolean.valueOf(isEditing).hashCode() * 29
					+ Boolean.valueOf(isEditingAgain).hashCode();
		}

		public boolean isEditing() {
			return isEditing;
		}

		public boolean isEditingAgain() {
			return isEditingAgain;
		}

		public void setEditing(boolean isEditing) {
			boolean wasEditing = this.isEditing;
			this.isEditing = isEditing;

			// This is a subsequent edit, so start from where we left off.
			if (!wasEditing && isEditing) {
				isEditingAgain = true;
				original = text;
			}
		}

		public void setText(String text) {
			this.text = text;
		
		}

		private boolean equalsOrBothNull(Object o1, Object o2) {
			return (o1 == null) ? o2 == null : o1.equals(o2);
		}
	}

	private static Template template;

	private final SafeHtmlRenderer<String> renderer;

	/**
	 * Construct a new EditTextCell that will use a
	 * {@link SimpleSafeHtmlRenderer}.
	 */
	public TaskGridNameCell() {
		this(SimpleSafeHtmlRenderer.getInstance());
	}

	/**
	 * Construct a new EditTextCell that will use a given
	 * {@link SafeHtmlRenderer} .
	 */
	public TaskGridNameCell(SafeHtmlRenderer<String> renderer) {
		super("click", "keyup", "keydown", "blur");
		if (template == null) {
			template = GWT.create(Template.class);
		}
		if (renderer == null) {
			throw new IllegalArgumentException("renderer == null");
		}
		this.renderer = renderer;
	}

	@Override
	public boolean isEditing(Element element, Task value, Object key) {
		ViewData viewData = getViewData(key);
		return viewData == null ? false : viewData.isEditing();
	}

	@Override
	public void onBrowserEvent(Element parent, Task value, Object key,
			NativeEvent event, ValueUpdater<Task> valueUpdater) {
		// System.out.println("event: "+event.getType()+", client x: " +
		// event.getClientX() + ",  screen x: "+event.getScreenX() +
		// "\n  parent left: " + parent.getAbsoluteLeft() +
		// ",  parent scroll left: " +parent.getScrollLeft());


		ViewData viewData = getViewData(key);
		if (viewData != null && viewData.isEditing()) {
			// Handle the edit event.
			editEvent(parent, key, viewData, event, valueUpdater);

		} else {
			String type = event.getType();
			int keyCode = event.getKeyCode();
			boolean enterPressed = "keyup".equals(type)
					&& keyCode == KeyCodes.KEY_ENTER;
			if ("click".equals(type) || enterPressed) {

				boolean buttonClicked = false;

//				if (viewData == null) {
					int buttonRight = parent.getFirstChildElement()
							.getFirstChildElement().getAbsoluteRight();
					int buttonLeft = parent.getFirstChildElement()
							.getFirstChildElement().getAbsoluteLeft();
					int mouseX = event.getClientX();
					buttonClicked = (value.isSummary() && buttonRight >= mouseX && buttonLeft <= mouseX);
//				}

				if (buttonClicked) {
					value.setCollapsed(!value.isCollapsed());
					onExpandCollapse(value);
					// Go into edit mode.
				} else if (viewData == null) {
					viewData = new ViewData(value);
					setViewData(key, viewData);
					edit(parent, value, key);
				} else {
					viewData.setEditing(true);
					edit(parent, value, key);
				}

			}
		}
	}
	
	public abstract void onExpandCollapse(Task task);

	@Override
	public void render(Task value, Object key, SafeHtmlBuilder sb) {
		// Get the view data.
		ViewData viewData = getViewData(key);
		if (viewData != null && !viewData.isEditing() && value != null
				&& value.equals(viewData.getTask())) {
			clearViewData(key);
			viewData = null;
		}

		if (viewData != null) {
			String text = viewData.getText();
			SafeHtml html = renderer.render(text);
			if (viewData.isEditing()) {
				// Note the template will not treat SafeHtml specially
				sb.append(template.input(html.asString()));
			} else {
				// The user pressed enter, but view data still exists.
				sb.append(html);
			}
		} else if (value != null) {
			SafeHtml html = renderer.render(value.getName());
			String style=(value.isCollapsed())?"expandButton":"collapseButton";
			style=(value.isSummary())?style:"noButton";
			sb.append(template.label(style,value.getLevel()*25,html.asString()));
		}
	}

	@Override
	public boolean resetFocus(Element parent, Task value, Object key) {
		if (isEditing(parent, value, key)) {
			getInputElement(parent).focus();
			return true;
		}
		return false;
	}

	/**
	 * Convert the cell to edit mode.
	 * 
	 * @param parent
	 *            the parent element
	 * @param value
	 *            the current value
	 * @param key
	 *            the key of the row object
	 */
	protected void edit(Element parent, Task value, Object key) {

		setValue(parent, value, key);
		InputElement input = getInputElement(parent);
		input.focus();
		input.select();
	}

	/**
	 * Convert the cell to non-edit mode.
	 * 
	 * @param parent
	 *            the parent element
	 * @param value
	 *            the current value
	 */
	private void cancel(Element parent, Task value) {
		clearInput(getInputElement(parent));
		setValue(parent, value, null);
	}

	/**
	 * Clear selected from the input element. Both Firefox and IE fire spurious
	 * onblur events after the input is removed from the DOM if selection is not
	 * cleared.
	 * 
	 * @param input
	 *            the input element
	 */
	private native void clearInput(Element input) /*-{
													if (input.selectionEnd)
													input.selectionEnd = input.selectionStart;
													else if ($doc.selection)
													$doc.selection.clear();
													}-*/;

	/**
	 * Commit the current value.
	 * 
	 * @param parent
	 *            the parent element
	 * @param viewData
	 *            the {@link ViewData} object
	 * @param valueUpdater
	 *            the {@link ValueUpdater}
	 */
	private void commit(Element parent, ViewData viewData,
			ValueUpdater<Task> valueUpdater) {
		Task value = updateViewData(parent, viewData, false);
		value.setName(viewData.getText());
		clearInput(getInputElement(parent));
		setValue(parent, value, viewData);
		valueUpdater.update(value);
	}

	private void editEvent(Element parent, Object key, ViewData viewData,
			NativeEvent event, ValueUpdater<Task> valueUpdater) {
		String type = event.getType();
		boolean keyUp = "keyup".equals(type);
		boolean keyDown = "keydown".equals(type);
		if (keyUp || keyDown) {
			int keyCode = event.getKeyCode();
			if (keyUp && keyCode == KeyCodes.KEY_ENTER) {
				// Commit the change.
				commit(parent, viewData, valueUpdater);
			} else if (keyUp && keyCode == KeyCodes.KEY_ESCAPE) {
				// Cancel edit mode.
				Task originalText = viewData.getTask();
				if (viewData.isEditingAgain()) {
					viewData.setText(originalText.getName());
					viewData.setEditing(false);
				} else {
					setViewData(key, null);
				}
				cancel(parent, originalText);
			} else {
				// Update the text in the view data on each key.
				updateViewData(parent, viewData, true);
			}
		} else if ("blur".equals(type)) {
			// Commit the change. Ensure that we are blurring the input element
			// and
			// not the parent element itself.
			EventTarget eventTarget = event.getEventTarget();
			if (Element.is(eventTarget)) {
				Element target = Element.as(eventTarget);
				if ("input".equals(target.getTagName().toLowerCase())) {
					commit(parent, viewData, valueUpdater);
				}
			}
		}
	}

	/**
	 * Get the input element in edit mode.
	 */
	private InputElement getInputElement(Element parent) {
		return parent.getFirstChild().<InputElement> cast();
	}

	/**
	 * Update the view data based on the current value.
	 * 
	 * @param parent
	 *            the parent element
	 * @param viewData
	 *            the {@link ViewData} object to update
	 * @param isEditing
	 *            true if in edit mode
	 * @return the new value
	 */
	private Task updateViewData(Element parent, ViewData viewData,
			boolean isEditing) {
		InputElement input = (InputElement) parent.getFirstChild();
		String value = input.getValue();
		viewData.setText(value);
		viewData.setEditing(isEditing);
		return viewData.getTask();
	}

	// abstract boolean isCollapsed();
	//
	// abstract boolean hasChildren();

}

